<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mvvm</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name" name="name"/>
        <input type="checkbox" value="Jack" v-model="checkedNames">
        <label for="jack">Jack</label>
        <input type="checkbox" value="Mike" v-model="checkedNames">
        <label for="mike">Mike</label>
        <select v-model="selected">
            <option value="">请选择学校</option>
            <option value="a">学校A</option>
            <option value="b">学校B</option>
        </select>
        <input type="hidden" value="false" id="d">
    </div>
    <script>
        ;(function(global,factory){
            typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
            typeof define === 'function' && define.amd ? define(factory) :
            (global = global || self, global.Vue = factory());
        }(this,function(){

            let Vue = function(options){
                this.$data = options.data;
                this.$el = Vue.utils.getEl(options.el);
                //数据劫持
                new Observer(this.$data);
                //编译
                new Complier(this);
                //数据代理
                this.dataProxy(this.$data);
            }

            Vue.utils = {
                getEl(selector){
                    !selector.nodeType && (
                        selector = /^#/.test(selector) ? document.querySelector(selector) : document.querySelectorAll(selector)
                    )
                    return selector;
                }
                ,getVal(data,expr){
                    expr.split(".").forEach(arg=>{
                        data = data[arg]
                    })
                    return data;
                }
                ,setVal(data,expr,value){
                    let args = expr.split(".")
                    args.forEach((arg,i)=>{
                        if(args.length - 1 == i){
                            return data[arg] = value;
                        }
                        data = data[arg]
                    })
                }
                ,isDirect(str){
                    return /^v-/.test(str);
                }
                ,delete(target,propOrIndex){
                    if(Array.isArray(target)){
                        let index = /^\d+$/.test(propOrIndex) ? propOrIndex : target.indexOf(propOrIndex);
                        target.splice(index,1);
                    }else{
                        delete target[propOrIndex];
                    }
                    return target;
                }
                ,set(target,propOrIndex,val){
                    if(Array.isArray(target)){
                        let index = /^\d+$/.test(propOrIndex) ? propOrIndex : target.indexOf(propOrIndex);
                        target.splice(index,1,val);
                    }else{
                        target[propOrIndex] = val;
                    }
                    return target;
                }
                ,def(obj,key,val,enumerable){
                    Object.defineProperty(obj,key,{
                        enumerable: !!enumerable,
                        configurable: true,
                        writable: true,
                        value:val
                    })
                }
            }

            Vue.prototype = {
                constructor:Vue
                ,dataProxy(data){
                    Object.keys(data).forEach(key=>{
                        Object.defineProperty(this,key,{
                            get(){
                                return data[key];
                            }
                            ,set(newVal){
                                data[key] = newVal;
                            }
                        })
                    })
                }
            }

            ,arrayProto = Array.prototype

            ,arrayProxy = Object.create(arrayProto)

            ,hasProto = '__proto__' in {}

            ,arrayMethods = ['push','pop','shift','unshift','splice','sort','reverse']

            arrayMethods.forEach(function(method){
                let original = arrayProto[method];
                //定义属性
                Vue.utils.def(arrayProxy,method,function(...args){
                    let result = original.apply(this, args)
                    return result
                })
            })

            ,names = Object.getOwnPropertyNames(arrayProxy)

            //劫持器（拦截器）
            ,Observer = function(data){
                this.data = data;
                this.observe(this.data);
            }

            //订阅者
            ,Subscribe = function(vm,node,expr){
                this.vm = vm;
                this.node = node;
                this.expr = expr;
                this.oldVal = this.get();
                this.matcher = new Matcher(node,this.oldVal,this);
            }

            //依赖收集
            ,Dep = function(){
                this.subs = [];
            }
            
            ,Complier = function(vm){
                this.vm = vm;
                this.$el = vm.$el
                this.complie()
            }
            
            //匹配器对象
            ,Matcher = function(node,value,sub){
                this.node = node;
                this.type = node.type;
                this.value = value;
                this.data = sub.vm.$data;
                this.expr = sub.expr;
                this.match[this.type].call(this.node,this.value,this.data,this.expr);
            }

            Observer.prototype = {
                constructor:Observer
                ,observe(obj){
                    if(Array.isArray(obj)){
                        //让数组原型的方法，重新指向新的对象
                        if(hasProto){
                            obj.__proto__ = arrayProxy;
                        }else{       
                            for(let i = 0, l = names.length; i < l; i++) {
                                let name = names[i]
                                Vue.utils.def(obj,name,arrayProxy[name]);
                            }
                        }
                    }else{
                        Object.keys(obj).forEach(key=>{
                            this.reactive(obj,key,obj[key])
                        })
                    }
                }
                //响应式函数
                ,reactive(obj,key,value){
                    typeof value == 'object' && this.observe(value)
                    let dep = new Dep();
                    Object.defineProperty(obj,key,{
                        get(){
                            dep.depend();
                            return value;
                        }
                        ,set(newVal){
                            if(value == newVal){return;}
                            typeof newVal == 'object' && this.observe(newVal)
                            value = newVal;
                            dep.notify(value)
                        }
                    })
                }
            }

            Matcher.prototype = {
                constructor:Matcher
                ,match:{
                    'text':function(value,data,expr){
                        this.value = value;
                        this.addEventListener('input',function(){
                            Vue.utils.setVal(data,expr,this.value)
                        })
                    }
                    ,'textarea':function(){
                        this.value = value;
                        this.addEventListener('input',function(){
                            Vue.utils.setVal(data,expr,this.value)
                        })
                    }
                    ,'checkbox':function(value,data,expr){
                        value.indexOf(this.value) > -1 && (this.checked = true)
                        this.addEventListener('change',function(e){
                            console.log(this.checked);
                            !this.checked && Vue.utils.delete(value,this.value)
                            let args = expr.split(".")
                            args.forEach((arg,i)=>{
                                if(args.length - 1 == i){
                                    return data[arg] = value;
                                }
                                data = data[arg]
                            })
                        })
                    }
                    ,'radio':function(){
                        this.value == value && (this.checked = true)
                        this.addEventListener('change',function(){
                            Vue.utils.setVal(data,expr,this.value)
                        })
                    }
                    ,'select-one':function(value,data,expr){
                        value.indexOf(this.value) > -1 && (this.checked = true)
                        this.addEventListener('change',function(e){
                            !this.checked && Vue.utils.delete(value,this.value)
                            value = this.options[this.selectIndex].value;
                            let args = expr.split(".")
                            args.forEach((arg,i)=>{
                                if(args.length - 1 == i){
                                    return data[arg] = value;
                                }
                                data = data[arg]
                            })
                        })
                    }
                    ,'select-multiple':function(){

                    }
                }
            }

            Subscribe.prototype = {
                constructor:Subscribe
                ,get(){
                    Dep.target = this;
                    let value = Vue.utils.getVal(this.vm.$data,this.expr);
                    Dep.target = null;
                    return value;
                }
                ,update(msg){
                    if(msg == this.oldVal){return}
                    this.updateView[this.node.type].call(this.node,msg);
                    this.oldVal = msg;
                }
                ,updateView:{
                    'text':function(value){
                        this.value = value;
                    }
                    ,'textarea':function(value){
                        this.value = value;
                    }
                    ,'checkbox':function(value){
                        this.checked = value;
                    }
                    ,'select-one':function(value){
                        Array.prototype.forEach.call(this.options,function(option){
                            option.value == value && (option.selected = true)
                        })
                    }
                }
                
            }
            
            Dep.prototype = {
                constructor:Dep
                ,addSub(sub){
                    this.subs.indexOf(sub) <= -1 && this.subs.push(sub)
                }
                ,depend(){
                    Dep.target && this.addSub(Dep.target)
                }
                ,notify(msg){
                    this.subs.forEach(sub=>{
                        sub.update(msg);
                    })
                }
            }

            Complier.prototype = {
                constructor:Complier
                ,complie(){
                    let el = this.$el;
                    //,fragment = this.node2fragment(el);
                    [...el.childNodes].forEach(node=>{
                        if(node.nodeType == Node.ELEMENT_NODE){
                            [...node.attributes].forEach(attr=>{
                                let {name,value:expr} = attr;
                                if(Vue.utils.isDirect(name)){
                                    let direct = name.split('-')[1];
                                    this.directSet[direct].call(this.vm,node,expr)
                                }
                            })
                        }
                    })
                    //el.appendChild(fragment);
                }
                ,node2fragment(el){
                    let fragment = document.createDocumentFragment(),child;
                    while(child = el.firstChild){
                        fragment.appendChild(child);
                    }
                    return fragment;
                }
                ,directSet:{
                    model:function(node,expr){
                        new Subscribe(this,node,expr);
                    }
                }
            }

            Vue.Observer = Observer;

            return Vue;

        }))
    </script>
    <script>
        let vm = new Vue({
            el:'#app'
            ,data:{
                user:{
                    name:'ck'
                }
                ,checkedNames:['Mike','Jack']
                ,selected:'a'
            }
        })
    </script>
</body>
</html>